diff options
Diffstat (limited to 'app/[lng]/admin/nonsap-sync/page.tsx')
| -rw-r--r-- | app/[lng]/admin/nonsap-sync/page.tsx | 267 |
1 files changed, 267 insertions, 0 deletions
diff --git a/app/[lng]/admin/nonsap-sync/page.tsx b/app/[lng]/admin/nonsap-sync/page.tsx new file mode 100644 index 00000000..4cc78c27 --- /dev/null +++ b/app/[lng]/admin/nonsap-sync/page.tsx @@ -0,0 +1,267 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; + +interface SyncProgress { + tableName: string; + lastSyncDate: string; + currentPage: number; + totalProcessed: number; + status: 'running' | 'completed' | 'error' | 'skipped'; + lastError?: string; + syncType: 'full' | 'delta' | 'rebuild'; + startTime: number; + endTime?: number; + recordsSkipped?: number; +} + +interface SyncConfig { + pageSize: number; + batchSize: number; + maxWorkers: number; + deltaSyncEnabled: boolean; + cronSchedule: string; + autoRefreshInterval: number; + environment: string; +} + +export default function NONSAPSyncPage() { + const [syncProgress, setSyncProgress] = useState<SyncProgress[]>([]); + const [isLoading, setIsLoading] = useState(false); + const [lastUpdated, setLastUpdated] = useState<string>(''); + const [syncConfig, setSyncConfig] = useState<SyncConfig | null>(null); + + // 설정 정보 조회 + const fetchSyncConfig = async () => { + try { + const response = await fetch('/api/nonsap-sync/config'); + const result = await response.json(); + + if (result.success) { + setSyncConfig(result.data); + } else { + console.error('Failed to fetch sync config:', result.error); + } + } catch (error) { + console.error('Error fetching sync config:', error); + } + }; + + // 동기화 상태 조회 + const fetchSyncStatus = async () => { + try { + const response = await fetch('/api/nonsap-sync/status'); + const result = await response.json(); + + if (result.success) { + setSyncProgress(result.data); + setLastUpdated(result.timestamp); + } else { + console.error('Failed to fetch sync status:', result.error); + } + } catch (error) { + console.error('Error fetching sync status:', error); + } + }; + + // 수동 동기화 트리거 + const triggerSync = async () => { + setIsLoading(true); + try { + const response = await fetch('/api/nonsap-sync/trigger', { + method: 'POST' + }); + const result = await response.json(); + + if (result.success) { + // 상태 새로고침 + setTimeout(fetchSyncStatus, 2000); + } else { + alert('동기화 시작에 실패했습니다: ' + result.error); + } + } catch (error) { + console.error('Error triggering sync:', error); + alert('동기화 트리거 중 오류가 발생했습니다.'); + } finally { + setIsLoading(false); + } + }; + + // 상태별 배지 색상 + const getStatusBadge = (status: string) => { + switch (status) { + case 'running': + return <Badge variant="default" className="bg-blue-500">실행 중</Badge>; + case 'completed': + return <Badge variant="default" className="bg-green-500">완료</Badge>; + case 'error': + return <Badge variant="destructive">오류</Badge>; + case 'skipped': + return <Badge variant="outline" className="bg-yellow-100">완료됨</Badge>; + default: + return <Badge variant="secondary">알 수 없음</Badge>; + } + }; + + // Cron 스케줄을 사용자 친화적으로 변환 + const formatCronSchedule = (cronSchedule: string) => { + if (cronSchedule === '0 */30 * * * *') { + return '30분마다'; + } + if (cronSchedule === '0 0 1 * * *') { + return '매일 새벽 1시'; + } + return cronSchedule; // 기본값으로 원본 반환 + }; + + + + // 동기화 타입별 배지 색상 + const getSyncTypeBadge = (syncType: string) => { + switch (syncType) { + case 'delta': + return <Badge variant="default" className="bg-green-500">차분 동기화</Badge>; + case 'full': + return <Badge variant="default" className="bg-blue-500">전체 동기화</Badge>; + case 'rebuild': + return <Badge variant="default" className="bg-orange-500">삭제 후 재구성</Badge>; + default: + return <Badge variant="secondary">{syncType}</Badge>; + } + }; + + // 초기 데이터 로드 + useEffect(() => { + fetchSyncConfig(); + fetchSyncStatus(); + }, []); + + // 자동 새로고침 + useEffect(() => { + if (!syncConfig) return; + + const interval = setInterval(fetchSyncStatus, syncConfig.autoRefreshInterval); + return () => clearInterval(interval); + }, [syncConfig]); + + return ( + <div className="container mx-auto p-6"> + <div className="flex justify-between items-center mb-6"> + <div> + <h1 className="text-3xl font-bold">NONSAP 데이터 동기화</h1> + <p className="text-muted-foreground"> + Oracle DB와 PostgreSQL 간의 데이터 동기화 상태를 모니터링합니다. + </p> + </div> + <div className="flex gap-2"> + <Button onClick={fetchSyncStatus} variant="outline"> + 새로고침 + </Button> + <Button onClick={triggerSync} disabled={isLoading}> + {isLoading ? '실행 중...' : '수동 동기화'} + </Button> + </div> + </div> + + {lastUpdated && ( + <Card className="mb-6"> + <CardContent className="pt-6"> + <p className="text-sm text-muted-foreground"> + 마지막 업데이트: {new Date(lastUpdated).toLocaleString('ko-KR')} + </p> + </CardContent> + </Card> + )} + + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> + {syncProgress.map((progress) => ( + <Card key={progress.tableName}> + <CardHeader> + <div className="flex justify-between items-start"> + <CardTitle className="text-lg">{progress.tableName}</CardTitle> + <div className="flex gap-2"> + {getSyncTypeBadge(progress.syncType)} + {getStatusBadge(progress.status)} + </div> + </div> + <CardDescription> + 마지막 동기화: {new Date(progress.lastSyncDate).toLocaleString('ko-KR')} + </CardDescription> + </CardHeader> + <CardContent> + <div className="space-y-2"> + <div className="flex justify-between"> + <span className="text-sm text-muted-foreground">현재 페이지:</span> + <span className="text-sm font-medium">{progress.currentPage}</span> + </div> + <div className="flex justify-between"> + <span className="text-sm text-muted-foreground">처리된 레코드:</span> + <span className="text-sm font-medium">{progress.totalProcessed.toLocaleString()}</span> + </div> + {progress.recordsSkipped && progress.recordsSkipped > 0 && ( + <div className="flex justify-between"> + <span className="text-sm text-muted-foreground">스킵된 레코드:</span> + <span className="text-sm font-medium">{progress.recordsSkipped.toLocaleString()}</span> + </div> + )} + {progress.endTime && progress.startTime && ( + <div className="flex justify-between"> + <span className="text-sm text-muted-foreground">실행 시간:</span> + <span className="text-sm font-medium"> + {((progress.endTime - progress.startTime) / 1000).toFixed(2)}초 + </span> + </div> + )} + {progress.lastError && ( + <div className="mt-2"> + <p className="text-sm text-red-600 break-words"> + 오류: {progress.lastError} + </p> + </div> + )} + </div> + </CardContent> + </Card> + ))} + </div> + + {syncProgress.length === 0 && ( + <Card> + <CardContent className="pt-6"> + <p className="text-center text-muted-foreground"> + 동기화 정보가 없습니다. 동기화를 실행해보세요. + </p> + </CardContent> + </Card> + )} + + {syncConfig && ( + <Card className="mt-6"> + <CardHeader> + <CardTitle>동기화 설정</CardTitle> + <CardDescription> + 테이블별 최적 동기화 방식을 자동 선택하여 실행됩니다. + 멀티스레드로 병렬 처리하여 성능을 최적화합니다. + </CardDescription> + </CardHeader> + <CardContent> + <div className="space-y-2 text-sm"> + <p><strong>실행 환경:</strong> {syncConfig.environment}</p> + <p><strong>동기화 방식:</strong> 테이블별 자동 선택 (차분/전체/재구성)</p> + <p><strong>차분 동기화:</strong> {syncConfig.deltaSyncEnabled ? '활성화' : '비활성화'}</p> + <p><strong>실행 주기:</strong> {formatCronSchedule(syncConfig.cronSchedule)}</p> + <p><strong>페이지 크기:</strong> {syncConfig.pageSize.toLocaleString()}개 레코드</p> + <p><strong>배치 크기:</strong> {syncConfig.batchSize.toLocaleString()}개 레코드</p> + <p><strong>워커 수:</strong> {syncConfig.maxWorkers}개 (병렬 처리)</p> + <p><strong>자동 새로고침:</strong> {(syncConfig.autoRefreshInterval / 1000)}초마다</p> + <p><strong>동기화 대상:</strong> {syncProgress.length}개 테이블</p> + </div> + </CardContent> + </Card> + )} + </div> + ); +}
\ No newline at end of file |
